<?php

class Bootstrap
{
    const ERROR_REPORTING_NONE = 0;
    const ENVIRONMENT = 'APPLICATION_STATE';
    const APPLICATION_STATE = 'production';
    const CONFIG_PATH = '../application/config/';
    const BOOTSTRAP_CONFIG = 'bootstrap';

    /**
     * Singelton instance
     *
     * @var Bootstrap
     */
    private static $_instance = null;

    /**
     * @var string
     */
    private $_applicationState = null;

    /**
     * @var Zend_Config
     */
    private $_bootstrapConfig = null;

    /**
     * @var array
     */
    private $_configFileTypes = array(
        'ini' => 'Zend_Config_Ini',
        'xml' => 'Zend_Config_Xml',
    );

    /**
     * @var string
     */
    private $_registryPrefix = 'MyProject';

    /**
     * @var Zend_Controller_Front
     */
    private $_frontController = null;

    /**
     * @var Zend_Db
     */
    private $_databaseAdapter = null;

    /**
     * Singelton pattern implementation makes "new" unavailable
     *
     * @return void
     */
    private function __construct()
    {}

    /**
     * Singelton pattern implementation makes "clone" unavailable
     *
     * @return void
     */
    private function __clone()
    {}

    /**
     * Singelton instance
     *
     * @return Bootstrap
     */
    public static function getInstance()
    {
        if (null === self::$_instance) {
            self::$_instance = new self();
        }
        return self::$_instance;
    }

    /**
     * @param string $state
     * @param string|Zend_Config $config
     * @return Bootstrap
     */
    public function init($state = null, $config = null)
    {
        $this->_addAutoload()
            ->_setApplicationState($state)
            ->_setBootstrapConfig($config)
            ->_setErrorReporting()
            ->_setDefaultTimezone();
        if (isset($this->_bootstrapConfig->includePaths)) {
        	$this->addPathsToIncludePath($this->_bootstrapConfig->includePaths->toArray());
        }
    }

    /**
     * Sets application state
     *
     * @param string $applicationState
     * @return Bootstrap
     */
    private function _setApplicationState($applicationState = null)
    {
        if (getenv(self::ENVIRONMENT)) {
            $this->_applicationState = getenv(self::ENVIRONMENT);
        } elseif (null === $applicationState) {
            $this->_applicationState = self::APPLICATION_STATE;
        } elseif (is_string($applicationState)) {
            $this->_applicationState = $applicationState;
        } else {
            throw new Exception(__METHOD__ . ' expects a string');
        }

        return $this;
    }

    /**
     * Gets application state
     *
     * @return string
     */
    public function getApplicationState()
    {
        return $this->_applicationState;
    }

    /**
     * Gets debug state
     *
     * @return boolean
     */
    public function getDebugState()
    {
        if (isset($this->_bootstrapConfig->debug)) {
            return (bool) $this->_bootstrapConfig->debug;
        } else {
            return false;
        }
    }

    /**
     * Sets error reporting level
     *
     * @return Bootstrap
     */
    private function _setErrorReporting()
    {
        if (true === $this->getDebugState()) {
            $level = E_ALL|E_STRICT;
        } else {
            $level = self::ERROR_REPORTING_NONE;
        }
        error_reporting($level);

        return $this;
    }

    /**
     * Adds a path to includepath
     *
     * @param string $path
     * @return Bootstrap
     */
    public function addPathToIncludePath($path)
    {
        set_include_path(get_include_path() . PATH_SEPARATOR . $path);
        return $this;
    }

    /**
     * Adds paths to includepath
     *
     * @param array $paths
     * @return Bootstrap
     */
    public function addPathsToIncludePath(array $paths)
    {
        $this->addPathsToIncludePath(implode(PATH_SEPARATOR, $paths));
        return $this;
    }

    /**
     * @return Bootstrap
     */
    private function _addAutoload($class = 'Zend_Loader')
    {
        require_once 'Zend/Loader.php';
        Zend_Loader::registerAutoload($class);

        return $this;
    }

    /**
     * Sets registry prefix
     *
     * @param string $registryPrefix
     * @return Bootstrap
     */
    public function setRegistryPrefix($registryPrefix)
    {
        $this->_registryPrefix = ucfirst($registryPrefix) . '_' ;
        return $this;
    }

    /**
     * Adds enrty to registry
     *
     * @param string $index
     * @param mixed $value
     * @return Bootstrap
     */
    private function _addToRegistry($index, $value)
    {
        require_once 'Zend/Registry.php';
        Zend_Registry::set($this->_registryPrefix . ucfirst($index), $value);

        return $this;
    }

    /**
     * Sets default timezone
     *
     * @return Bootstrap
     */
    private function _setDefaultTimezone()
    {
        date_default_timezone_set($this->_bootstrapConfig->date->timezone);
        return $this;
    }

    /**
     * Sets session
     *
     * @return Bootstrap
     */
    public function setSession()
    {
        try {
            $config = $this->loadConfig('session');
            /**
             * @see Zend_Session
             */
            require_once 'Zend/Session.php';
            Zend_Session::start($config->toArray);
        } catch (Zend_Session_Exception $exception) {
            throw new Exception(__METHOD__ . ' - ' . $exception->getMessage());
        }

        return $this;
    }

    /**
     * Sets database adapter
     *
     * @throws Exception
     * @return Bootstrap
     */
    public function setDatabaseAdapter()
    {
        try {
            $config = $this->loadConfig('database');
            /**
             * @see Zend_Db
             */
            require_once 'Zend/Db.php';
            $this->_databaseAdapter = Zend_Db::factory($config);
        } catch (Zend_Db_Exception $excpetion) {
            throw new Exception(__METHOD__ . ' - ' . $excpetion->getMessage());
        }

        return $this;
    }

    /**
     * Gets database adapter
     *
     * @throws Excpetion
     * @return Zend_Db
     */
    public function getDatabaseAdapter()
    {
        if (null === $this->_databaseAdapter) {
            throw new Exception('Database adapter not set');
        }
        return $this->_databaseAdapter;
    }

    /**
     * @return Bootstrap
     */
    public function addDatabaseTableInterface()
    {
        if (null === $this->_databaseAdapter) {
            $this->setDatabaseAdapter();
        }

        require_once 'Zend/Db/Table/Abstract.php';
        Zend_Db_Table_Abstract::setDefaultAdapter($this->_databaseAdapter);

        return $this;
    }

    /**
     * @return Bootstrap
     */
    public function setFrontController()
    {
        $config = $this->_bootstrapConfig->controller;

        /**
         * @see Zend_Controller_Front
         */
        require_once 'Zend/Controller/Front.php';
        $frontController = Zend_Controller_Front::getInstance();
        $frontController->addModuleDirectory($config->moduleDirectory)
            ->setParams($config->params->toArray())
            ->returnResponse((bool) $config->returnResponse)
            ->throwExceptions((bool) $config->throwExcpetions);
        if (isset($config->baseUrl)) {
        	$frontController->setBaseUrl($config->baseUrl);
        }
        $this->_frontController = $frontController;

        return $this;
    }

    /**
     * @return Zend_Controller_Front
     */
    public function getFrontController()
    {
        return $this->_frontController;
    }

    /**
     * Dispatches the front controller
     *
     * @throws Exception
     * @return void
     */
    public function dispatch()
    {
        if (null === $this->_frontController) {
            $this->setFrontController();
        }

        $response = $this->_frontController->dispatch();
        if (null !== $response) {
            if ($response->isException()) {
                $exception = $response->getException();
                throw $exception;
            }
            $response->outputBody();
        }
    }

    /**
     * Registers a plugin
     *
     * @throws Exception
     * @return Bootstrap
     */
    public function registerPlugin()
    {
        if (null ===$this->_frontController) {
            throw new Exception('Front controller not set');
        }

        $args = func_get_args();
        $class = array_shift($args);

        if ( ! class_exists($class)) {
            throw new Exception("Plugin \"$class\" does not exist");
        }

        $reflection = new ReflectionClass($class);
        if (count($args) > 0) {
            $plugin = $reflection->newInstanceArgs($args);
        } else {
        	$plugin = $reflection->newInstance();
        }
        $this->_frontController->registerPlugin($plugin);

        return $this;
    }

    /**
     * Adds routes
     */
    public function addRoutes()
    {
        if (null === $this->_frontController) {
            throw new Exception('Front controller not set');
        }
        $config = $this->loadConfig('routes');
        $this->_frontController->getRouter()->addConfig($config, 'routes');
    }

    /**
     * loadConfig
     *
     * @param string $requestConfig
     * @param string $registryIndex
     * @throws Exception
     * @return Zend_Config
     */
    public function loadConfig($requestConfig, $section = null, $registryIndex = null)
    {
        $config = null;
        if (null === $section) {
            $section = $this->_applicationState;
        }

        foreach ($this->_configFileTypes as $ext => $class) {
            $configFile = (string) self::CONFIG_PATH . $requestConfig . '.' . $ext;
        	if (file_exists($configFile)) {
        	    /**
        	     * @see Zend_Loader
        	     */
                require_once 'Zend/Loader.php';
        	    Zend_Loader::loadClass($class);
        	    $config = new $class($configFile, $section);
        	    break;
        	}
        }

        if (null === $config && isset($this->_bootstrapConfig->$requestConfig)) {
            $config = $this->_bootstrapConfig->$requestConfig;
        }

        if ( ! $config instanceof Zend_Config) {
            throw new Exception("Config \"$requestConfig\" not found");
        }

        if (null !== $registryIndex){
            $this->_addToRegistry($registryIndex, $config);
        }

        return $config;
    }

    /**
     * @param string|Zend_Config
     * @throws Exception
     * @return Bootstrap
     */
    private function _setBootstrapConfig($bootstrapConfig = null)
    {
        if (null === $bootstrapConfig) {
            $this->_bootstrapConfig = $this->loadConfig(self::BOOTSTRAP_CONFIG);
        } elseif (is_string($bootstrapConfig)) {
            $this->_bootstrapConfig = $this->loadConfig($bootstrapConfig);
        } elseif ($bootstrapConfig instanceof Zend_Config) {
        	$this->_bootstrapConfig = $bootstrapConfig;
        } else {
            throw new Exception('Bootstrap config could not be loaded');
        }

        return $this;
    }
}
